Acknowledgement

Shiny & bootstrap

  • Much of the interface provided by Shiny is based on the html elements, styling, and javascript provided by the Bootstrap library.

  • Knowing the specifics of html (and Bootstrap specifically) are not needed for working with Shiny -but understanding some of its conventions goes a long way to helping you customise the elements of your app (via custom CSS and other tools).

  • This is not the only place that Bootstrap shows up in the R ecosystem - both RMarkdown and Quarto html documents use Bootstrap for styling as well.

bslib

The bslib R package provides a modern UI toolkit for Shiny, R Markdown, and Quarto based on Bootstrap.

It provides,

  • Custom theming of Shiny apps and R Markdown documents

  • Switch between different versions of Bootstrap

  • Modern UI components like cards, value boxes, sidebars, and more.

This last set of features is what we will focus on now, with more on the first two in next module.

Cards

  • Cards are a UI element that you will recognize from many modern websites.
  • They are rectangular containers with borders and padding that are used to group related information.
  • When utilised properly to group related information, they help users better digest, engage, and navigate through content
card(
  card_header(
    "A header"
  ),
  card_body(
    shiny::markdown(
      "Some **bold** text"
    )
  )
)

Styling cards

  • Cards can be styled using the class argument
    • this is used to apply Bootstrap classes to the card and its components.
card(
  max_height = 250,
  card_header(
    "Long scrollable text",
    class = "bg-primary"
  ),
  card_body(
    lorem::ipsum(paragraphs = 3, sentences = 5),
    class = "bg-info"
  )
)

Multiple card bodies

  • Cards are also super flexible and can contain multiple card_body() elements.
    • This can be useful for creating complex layouts.
card(
  max_height = 450,
  card_header(
    "Text and a map!",
    class = "bg-dark"
  ),
  card_body(
    max_height = 200, 
    class = "p-0",
    leaflet::leaflet() |>
      leaflet::addTiles()
  ),
  card_body(
    lorem::ipsum(
      paragraphs = 1, 
      sentences = 3
    )
  )
)

Value boxes

  • Value boxes are the other major UI component provided by bslib.
  • They are a simple way to display a value and a label in a styled box.
  • They are often used to display key metrics in a dashboard.
value_box(
  title = "1st value",
  value = 123,
  showcase = bs_icon("bar-chart"),
  theme = "primary",
  p("The 1st detail")
)

value_box(
  title = "2nd value",
  value = 456,
  showcase = bs_icon("graph-up"),
  theme = "secondary",
  p("The 2nd detail"),
  p("The 3rd detail")
)

Demo 06 - Shiny + bslib

demos/demo06.R

library(tidyverse)
library(shiny)
library(bslib)

d <- readr::read_csv(here::here("data/weather.csv"))

d_vars = c("Average temp" = "temp_avg",
           "Min temp" = "temp_min",
           "Max temp" = "temp_max",
           "Total precip" = "precip",
           "Snow depth" = "snow",
           "Wind direction" = "wind_direction",
           "Wind speed" = "wind_speed",
           "Air pressure" = "air_press",
           "Total sunshine" = "total_sun")

ui <- page_sidebar(
  title = "Weather Data",
  sidebar = sidebar(
    selectInput(
      "region", "Select a region", 
      choices = c("West", "Midwest", "Northeast", "South")
    ),
    selectInput(
      "name", "Select an airport", choices = c()
    ),
    selectInput(
      "var", "Select a variable",
      choices = d_vars, selected = "temp_avg"
    )
  ),
  card(
    card_header(
      textOutput("title")
    ),
    card_body(
      plotOutput("plot")
    )
  )
)


server <- function(input, output, session) {
  observe({
    updateSelectInput(
      session, "name",
      choices = d |>
        distinct(region, name) |>
        filter(region == input$region) |>
        pull(name)
    )
  })
  
  output$title = renderText({
    names(d_vars)[d_vars==input$var]
  })
  
  d_city = reactive({
    req(input$name)
    d |>
      filter(name %in% input$name)
  })
  
  output$plot = renderPlot({
    d_city() |>
      ggplot(aes(x=date, y=.data[[input$var]])) +
      geom_line() +
      theme_minimal()
  })
}

shinyApp(ui = ui, server = server)

Dynamic UIs

Adding value boxes

Previously we had included a table that showed minimum and maximum temperatures - lets try presenting these using value boxes instead.

Before we get to the code lets think a little bit about how we might do this.

value_box(
  title = "Average Temp",
  value = textOutput("avgtemp"),
  showcase = bsicons::bs_icon("thermometer-half"),
  theme = "success"
)


Any one see a potential issue with this?

uiOutput() and renderUI()

  • To handle situations like this Shiny provides the ability to dynamically generate UI elements entirely within the server function.

  • For our case we can create all of the value boxes we need in a single renderUI() call making our code simpler and more maintainable.

  • Additionally, since renderUI() is a reactive context we can perform all of our calculations in the same place.

Demo 07 - Value boxes

demos/demo07.R

library(tidyverse)
library(shiny)
library(bslib)

d <- readr::read_csv(here::here("data/weather.csv"))

d_vars <- c("Average temp" = "temp_avg",
           "Min temp" = "temp_min",
           "Max temp" = "temp_max",
           "Total precip" = "precip",
           "Snow depth" = "snow",
           "Wind direction" = "wind_direction",
           "Wind speed" = "wind_speed",
           "Air pressure" = "air_press",
           "Total sunshine" = "total_sun")

ui <- page_sidebar(
  title = "Weather Data",
  sidebar = sidebar(
    selectInput(
      "region", "Select a region", 
      choices = c("West", "Midwest", "Northeast", "South")
    ),
    selectInput(
      "name", "Select an airport", choices = c()
    ),
    selectInput(
      "var", "Select a variable",
      choices = d_vars, selected = "temp_avg"
    )
  ),
  card(
    card_header(
      textOutput("title")
    ),
    card_body(
      plotOutput("plot")
    )
  ),
  uiOutput("valueboxes")
)

server <- function(input, output, session) {
  observe({
    updateSelectInput(
      session, "name",
      choices = d |>
        distinct(region, name) |>
        filter(region == input$region) |>
        pull(name)
    )
  })
  
  output$valueboxes = renderUI({
    clean = function(x) {
      round(x,1) |> paste("°C")
    }
    
    layout_columns(
      value_box(
        title = "Average Temp",
        value = mean(d_city()$temp_avg, na.rm=TRUE) |> clean(),
        showcase = bsicons::bs_icon("thermometer-half"),
        theme = "success"
      ),
      value_box(
        title = "Minimum Temp",
        value = min(d_city()$temp_min, na.rm=TRUE) |> clean(),
        showcase = bsicons::bs_icon("thermometer-snow"),
        theme = "primary"
      ),
      value_box(
        title = "Maximum Temp",
        value = max(d_city()$temp_max, na.rm=TRUE) |> clean(),
        showcase = bsicons::bs_icon("thermometer-sun"),
        theme = "danger"
      )
    )
  })
  
  output$title = renderText({
    names(d_vars)[d_vars==input$var]
  })
  
  d_city = reactive({
    req(input$name)
    d |>
      filter(name %in% input$name)
  })
  
  output$plot = renderPlot({
    d_city() |>
      ggplot(aes(x=date, y=.data[[input$var]])) +
      geom_line() +
      theme_minimal()
  })
}

shinyApp(ui = ui, server = server)

Your turn - Exercise 05

Using the code provided in exercise/ex05.R (based on demo/demo09.R) as a starting point

  • change the plot to a plotly object
  • also explore different design options for the value boxes

Hint: the Build-A-Box app provides inspiration for the design of the value boxes

shiny::runExample("build-a-box", package = "bslib")
10:00